Skip to content

fix(tools): clamp block max in block_fp8 to avoid NaN weights from all-zero blocks - #2256

Merged
zhuzilin merged 1 commit into
THUDM:mainfrom
hobostay:fix-block-fp8-zero-scale
Aug 12, 2026
Merged

zhuzilin merged 1 commit into
THUDM:mainfrom
hobostay:fix-block-fp8-zero-scale

Conversation

@hobostay

@hobostay hobostay commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Problem

block_fp8 — the default quantization strategy of tools/convert_hf_to_fp8.py — computes

scale = block_max.to(torch.float32) / FP8_MAX
qweight = (qweight / scale).clamp(...)

without clamping the block max away from zero, unlike channel_fp8 and tensor_fp8 in the same file, which both do .clamp(min=1e-12).

For an all-zero 128×128 block (padding vocab rows, unused MoE experts, zeroed gate weights — all present in large MoE checkpoints), block_max == 0 gives scale == 0 and qweight == 0 / 0 == NaN (torch.clamp(NaN) stays NaN). The NaN weights are silently written into the converted safetensors, and dequantization (NaN * 0 = NaN) then poisons the forward pass of the converted model. Near-zero blocks similarly yield inf, which gets clamped to ±448 — a wrong value with no error raised.

Reproduction (CPU):

w = torch.zeros(256, 256, dtype=torch.bfloat16)
w[0, 0] = 1.0  # only the first 128x128 tile is non-zero
q, s = block_fp8(w, (128, 128))
torch.isnan(q.float()).sum()  # 49152 NaNs on current main

Fix

Clamp the block max to 1e-12, exactly matching channel_fp8 / tensor_fp8:

scale = block_max.clamp(min=1e-12).to(torch.float32) / FP8_MAX

An all-zero block then quantizes to zeros with a tiny scale, and dequantizes back to exact zeros. Non-zero blocks are completely unaffected.

Tests

Added tests/test_block_fp8_zero_block.py (CPU, @pytest.mark.unit):

  • mixed zero/non-zero tiles → no NaN/inf in the output, all scales > 0 (fails without the fix);
  • all-zero block round-trips to exact zeros (fails without the fix);
  • non-zero blocks quantize/dequantize within FP8 tolerance (passes before and after — guards against behavior change).

All 3 tests pass locally, plus ruff and black --check are clean.

…l-zero blocks

block_fp8 (the default quantization strategy of convert_hf_to_fp8.py)
computed scale = block_max / FP8_MAX without clamping the block max
away from zero, unlike channel_fp8 and tensor_fp8 which both clamp to
1e-12. For an all-zero 128x128 block (padding vocab rows, unused MoE
experts, zeroed gate weights, ...), block_max == 0 gives scale == 0 and
qweight == 0/0 == NaN, silently writing NaN weights into the converted
checkpoint; dequantization then propagates NaN through the forward
pass. Near-zero blocks similarly produced inf that got clamped to
garbage values.

Clamp the block max to 1e-12, matching the other two strategies, and
add CPU unit tests that fail without the fix.
@zhuzilin
zhuzilin merged commit dd4851f into THUDM:main Aug 12, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants